package lift_simulator;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.CopyOnWriteArrayList;

public class CsvManager {
	private File inputFile;
	private File outputFile;
	private BufferedWriter writer;

	public CsvManager(String inputPath, String outputPath){
		inputFile = new File(inputPath);
		outputFile = new File(outputPath);
	}
	/**
	 * {@link #inputFile}を読み込み、{@link Task}のリストに変換します。
	 * @return
	 */
	public ArrayList<Task> importCsv(){
		ArrayList<Task> ret = new ArrayList<Task>();

		InputStream is = null;
		Reader r = null;
		BufferedReader br = null;
		try {
			is = new FileInputStream(inputFile);
			r  = new InputStreamReader(is, "SJIS");
			br = new BufferedReader(r);

			for (;;) {
				String text = br.readLine();
				if (text == null) break;
				String[] str = text.split(",");
				ret.add(new Task(str[0], str[1], str[2], str[3]));
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		} finally {
			if (br != null) try { br.close(); } catch (IOException e) {}
			if (r  != null) try { r .close(); } catch (IOException e) {}
			if (is != null) try { is.close(); } catch (IOException e) {}
		}
		return ret;
	}

	private void initWriter() throws UnsupportedEncodingException, FileNotFoundException{
		writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outputFile), "SJIS"));
	}

	public void log(LiftLog liftLog) throws IOException{
		if(writer==null)
			initWriter();
		writer.write(liftLog.toString());
		writer.newLine();
		writer.flush();
	}

	/**
	 * 入力データと、出力したデータに整合性がとれているかチェックします。
	 * また、全体でどれだけ時間がかかったかも計算します。
	 * @param path
	 */
	public void check(String path){
		System.out.println("【入力と出力のチェック開始】");
		//出力ファイルをリフトごとに分割する
		TreeMap<Integer,ArrayList<LiftLog>> output = divideOutput(path);
		//入力ファイルももう一度取得(シミュレーション中は開放したかった)
		ArrayList<Task> input = importCsv();
		IOChecker checker = new IOChecker(output, input);
		checker.check();
	}

	/**
	 * 指定されたパスの出力ファイルを取得し、リフトの識別番号毎に
	 * {@link TreeMap}<{@link ArrayList}<{@link LiftLog}>>形式で出力します。
	 * @param path
	 * @return
	 */
	private TreeMap<Integer,ArrayList<LiftLog>> divideOutput(String path) {
		TreeMap<Integer,ArrayList<LiftLog>> ret = new TreeMap<Integer, ArrayList<LiftLog>>();

		InputStream is = null;
		Reader r = null;
		BufferedReader br = null;
		try {
			is = new FileInputStream(path);
			r  = new InputStreamReader(is, "SJIS");
			br = new BufferedReader(r);
			int count = 0;
			for (;;) {
				String text = br.readLine();
				if (text == null) break;
				String[] str = text.split(",");
				int liftNum = Integer.parseInt(str[0]);
				if(ret.get(liftNum)==null)
					ret.put(liftNum, new ArrayList<LiftLog>());
				String[] tasks = new String[]{str[4],str[5],str[6],str[7],str[8]};
				LiftLog log = LiftLog.makeLiftLog(str[0], str[1], str[2], str[3], tasks);
				if(log!=null)
					ret.get(liftNum).add(log);
				else
					System.err.println("致命的なエラー："+count+"行目でエラーが発生しました");
				count++;
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		} finally {
			if (br != null) try { br.close(); } catch (IOException e) {}
			if (r  != null) try { r .close(); } catch (IOException e) {}
			if (is != null) try { is.close(); } catch (IOException e) {}
		}
		return ret;
	}
}

/**
 * 入力ファイルと出力ファイルの整合性をチェックするクラスです。
 * @author Yone6379univ
 *
 */
class IOChecker{
	TreeMap<Integer,ArrayList<LiftLog>> output;
	ArrayList<Task> input;
	/**
	 * 降車を確認済みのタスク
	 */
	Map<Integer,Task> boardingTask;
	/**
	 * 搭乗確認済みのタスク
	 */
	List<Task> waitingDescendTask;
	/**
	 * 各タスクの実際にかかった時間
	 */
	Map<Integer,Integer> resultTimes;
	public IOChecker(TreeMap<Integer, ArrayList<LiftLog>> output,
			ArrayList<Task> input) {
		this.output = output;
		this.input = input;
		boardingTask=new ConcurrentHashMap<Integer, Task>();//TODO マルチスレッドセーフにする必要あるか
		waitingDescendTask=new CopyOnWriteArrayList<Task>();
		resultTimes= new ConcurrentSkipListMap<Integer, Integer>();
	}

	/**
	 * チェックを開始します。チェックの流れとしては、<br>
	 * ・リフトごとに出力を分割<br>
	 * ・リフトごとにスレッドで並列チェック<br>
	 * 【チェック内容】開くと閉じるが交互に来ているか、閉じてから階*2秒で到着しているか、<br>
	 * 降車階、待機時間、時間と搭乗階、すべてのタスクが終了しているか<br>
	 * ・スレッドがすべて終了後に結果を出力<br>
	 * このような流れになっています。
	 */
	public void check(){
		//各リフトの振る舞いを各スレッドでチェックする
		ArrayList<Thread> threads = new ArrayList<Thread>();
		for(final Entry<Integer, ArrayList<LiftLog>> logsEntry : output.entrySet()){
			threads.add(new Thread(new CheckRunner(logsEntry, input, boardingTask, waitingDescendTask,resultTimes)));
		}

		//理論上の最短時間を計算
		int bestSum = 0;
		for(Entry<Integer, Integer> entry : checkBestTime().entrySet()){
			bestSum+=entry.getValue();
		}

		//同時にスタートさせる
		for(Thread thread : threads)
			thread.start();

		//すべてのスレッドが終わるまで待機
		try {
			for(Thread thread : threads)
				thread.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		//inputが残っていたら警告
		if(input.size()!=0){
			System.err.println("警告：消化できていないタスクが存在します。");
			for(Task task : input){
				System.err.println(task);
			}
		}

		//結果を出力
		int sum=0;
		System.out.println("【各タスクの時間一覧】");
		for(Entry<Integer, Integer> entry : resultTimes.entrySet()){
			System.out.println("id:"+entry.getKey()+", time:"+entry.getValue());
			sum+=entry.getValue();
		}
		System.out.println("合計:"+sum+"(理論最短時間:"+bestSum+")");
	}

	/**
	 * inputの理論上の最短時間を返します。<br>
	 * 各タスクの最短時間とは、エレベータが閉まる直前に搭乗し、他の階に止まること無く降車した場合の時間です。
	 * @return
	 */
	public Map<Integer,Integer> checkBestTime(){
		Map<Integer,Integer> ret = new TreeMap<Integer, Integer>();
		for(Task task : input){
			int time = Math.abs((task.getDescend()-task.getBoard())*2);
			ret.put(task.getId(), time);
		}
		return ret;
	}
}

/**
 * 実際に各スレッドが行うチェックアルゴリズムです
 * @author Yone6379univ
 *
 */
class CheckRunner implements Runnable{
	Entry<Integer, ArrayList<LiftLog>> logsEntry;
	ArrayList<Task> input;
	Map<Integer,Task> boardingTask;
	List<Task> waitingDescendTask;
	Map<Integer,Integer> resultTimes;

	public CheckRunner(Entry<Integer, ArrayList<LiftLog>> logsEntry,
			ArrayList<Task> input, Map<Integer,Task> boardingTask,
			List<Task> waitingDescendTask,Map<Integer,Integer> resultTimes) {
		this.logsEntry = logsEntry;
		this.input = input;
		this.boardingTask=boardingTask;
		this.waitingDescendTask=waitingDescendTask;
		this.resultTimes=resultTimes;
	}


	@Override
	public void run() {
		//ここからリフトごとにチェック
		int liftID = logsEntry.getKey();//リフトのID
		ArrayList<LiftLog> logs = logsEntry.getValue();//そのリフトのログ

		//開始時点のリフトログを作成する
		LiftLog beforeLog = LiftLog.makeLiftLog(String.valueOf(liftID), "0", "0", "E", new String[]{"0","0","0","0","0"});

		for(LiftLog log : logs){
			//開くと閉じるが交互に来ているかのチェック
			if(beforeLog.getActionName()==log.getActionName()){
				//交互に来ていなければ残念
				err(log,"E/Bが交互に来ていません");
			}			
			else{
				//交互に来ていれば
				int startTime = log.getActionTime();
				int actionTime = log.getActionTime()-beforeLog.getActionTime();
				int actionFloor = log.getActionFloor();

				switch (log.getActionName()) {
				case 'B':
					//今が開くの場合
					//閉じてから階*2秒で到着しているかのチェック TODO これが当てはまらない場合ってある？
					int movedFloor = Math.abs(beforeLog.getActionFloor()-actionFloor);
					if(actionTime != movedFloor*2){
						/*
						 * TODO エレベータが待機できることと現在の出力の仕様(開いた時と閉じた時しか記録しない)だと
						 * 移動時間を完全にチェックすることはできない。最低移動時間を守っているかということのみ。
						 */
						if(actionTime < movedFloor*2){
							//移動時間が正しく無ければ残念
							err(log,"移動時間の計算が合いません");
						}else{
							warning(log,"リフトを閉じてから移動開始までに待機時間があります");
						}
					}
					//放出したタスクをチェック。降車階。
					for(int taskID:log.getTaskIDs()){
						if(taskID==0)
							continue;
						Task task = getTask(input,taskID);
						if(task.getDescend()!=actionFloor){
							err(log,task+"の降車階が誤っています。");
						}
						//タスクのdescendResultにactionTimeを設定する
						task.setDescendResult(startTime);
						//タスクを開放待ちリストに追加
						waitingDescendTask.add(task);
						//開放待ちリストで搭乗済みタスクを検索
						//存在すれば時間計算→搭乗済み、開放待ち、inputから削除
						updateThreeList();
					}
					break;
				case 'E':
					//今が閉じるの場合
					//開いてから5秒以上待機してるかのチェック
					if(actionTime<=4){
						//待機時間が正しくなければ残念
						err(log,"待機時間が5秒未満です");
					}
					//格納したタスクをチェック。時間と搭乗階。
					for(int taskID:log.getTaskIDs()){
						if(taskID==0)
							continue;
						Task task = getTask(input,taskID);
						if(task.getBoard()!=actionFloor||task.getBoardTime()>startTime){
							err(log,task+"の階または搭乗時間が誤っています。");
						}
						//タスクのboardResultにactionTimeを設定する
						task.setBoardResult(startTime);
						//搭乗済みリストに格納する
						boardingTask.put(task.getId(), task);
					}
					break;
				default:
					//それ以外はありえない
					err(log,"E,B以外のコマンドが出力されています。");
					break;
				}
			}
			beforeLog = log;
		}
	}
	/**
	 * {@link #waitingDescendTask}内のタスクを利用して{@link #boardingTask}を検索し、
	 * 両者が一致した場合、時間計算を行った後、上記二つのリストとinputから該当するタスクを削除します。
	 */
	private void updateThreeList() {
		for(Task decsendTask:waitingDescendTask){
			Task boardTask = boardingTask.get(decsendTask.getId());
			//もし両者にタスクがあった場合
			if(boardTask!=null && decsendTask==boardTask){//TODO 危険か？
				//時間計算
				int resultTime = (decsendTask.getDescendResult()-boardTask.getBoardTime());
				resultTimes.put(decsendTask.getId(), resultTime);
				//３つのタスクから削除
				waitingDescendTask.remove(decsendTask);
				boardingTask.remove(boardTask.getId());
				input.remove(boardTask);
			}
		}
	}

	/**
	 * チェックに関するエラー文を出力します。
	 * @param log
	 * @param message
	 */
	public void err(LiftLog log, String message){
		System.err.println("警告："+log+"に誤りがあります. "+message);
	}
	/**
	 * チェックに関する警告文を出力します。
	 * @param log
	 * @param message
	 */
	public void warning(LiftLog log, String message){
		System.out.println("注意："+log+"に関して, "+message);
	}

	/**
	 * 指定されたリスト指定されたIDのタスクを取得します
	 * TODO Mapに格納しておけばよかった…
	 * @param taskID
	 * @return
	 */
	public Task getTask(ArrayList<Task> list,int taskID){
		Task target = new Task(String.valueOf(taskID), "0", "0", "0");
		return list.get(list.indexOf(target));
	}
}
